package com.access.boot.autoconfigure.apisdk.proxy;

import com.access.boot.apisdk.SdkContext;
import com.access.boot.autoconfigure.apisdk.exception.JavassistException;
import com.access.boot.autoconfigure.apisdk.exception.NotFoundAnyOpenApiDefinitionException;
import com.access.boot.autoconfigure.apisdk.exception.OverloadMethodsException;
import com.access.boot.autoconfigure.apisdk.util.EnhanceUtil;
import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.AttributeInfo;
import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.ParameterAnnotationsAttribute;
import javassist.bytecode.SignatureAttribute;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Yangzhen
 * @date 2022/6/3 18:51
 * @since 1.0.0
 */
@Slf4j
public class OpenApiEnhance {

    // 原始接口的 class
    private Class openApiInterface;
    // 原始接口的 CtClass
    private CtClass sourceCtClass;
    // 增强后的 CtClass
    private CtClass targetCtClass;
    // 增强后的 Class
    private Class enhanceInterface;

    // key：javassist 创建的方法签名
    // value：原始方法
    private Map<String, Method> targetKeySourceMethodMap = new HashMap<>();

    // key：sourceMethod
    // value：targetMethod
    private Map<Method, Method> methodMapping = new HashMap<>();

    private Map<Method, MethodWrapper> methodMethodWrapperMap = new HashMap<>();

    public OpenApiEnhance(Class openApiInterface) {
        this.openApiInterface = openApiInterface;
        checkDuplicateMethodName(openApiInterface);
        this.enhanceInterface = create$0(openApiInterface);
        dealMethodMapping();
    }

    /**
     * 处理方法映射
     */
    private void dealMethodMapping() {
        try {
            CtMethod[] declaredMethods = targetCtClass.getDeclaredMethods();
            for (CtMethod declaredMethod : declaredMethods) {
                // 原始方法
                Method sourceMethod = targetKeySourceMethodMap.get(declaredMethod.getMethodInfo2().toString());
                // 目标方法
                Method targetMethod = enhanceInterface.getDeclaredMethod(declaredMethod.getName(), getParameterTypes(declaredMethod));
                MethodWrapperCache.put(targetMethod, new MethodWrapper(targetMethod, needAutoType(sourceMethod)));
                methodMapping.put(sourceMethod, targetMethod);
                log.info("open api enhance method mapping: source method {}, target method {}", sourceMethod.toString(), targetMethod.toString());
            }
        } catch (NotFoundException | ClassNotFoundException | NoSuchMethodException e) {
            throw new JavassistException(e);
        }
    }

    /**
     * 获取参数类型
     *
     * @param declaredMethod
     * @return
     * @throws NotFoundException
     * @throws ClassNotFoundException
     */
    private Class[] getParameterTypes(CtMethod declaredMethod) throws NotFoundException, ClassNotFoundException {
        CtClass[] parameterTypes = declaredMethod.getParameterTypes();
        List<Class> parameterTypeList = new ArrayList<>();
        if (parameterTypes != null) {
            for (CtClass parameterType : parameterTypes) {
                Class<?> pt;
                if (parameterType.isPrimitive()) {
                    pt = EnhanceUtil.PRIMITIVE_MAPPING.get(parameterType.getName());
                } else {
                    pt = Class.forName(parameterType.getName());
                }
                parameterTypeList.add(pt);
            }
        }
        Class[] classes = new Class[parameterTypeList.size()];
        parameterTypeList.toArray(classes);
        return classes;
    }

    /**
     * 创建内存加载的类
     *
     * @param openApiInterface
     * @return
     */
    private Class create$0(Class openApiInterface) {

        try {
            // 获取原始类的 CtClass
            this.sourceCtClass = EnhanceUtil.pool.getCtClass(openApiInterface.getName());
            // 拷贝原始类，生成新的内存类，命名：$ + 原始类的名称
            CtClass targetCtClass = EnhanceUtil.copyCtClass(openApiInterface);
            if (targetCtClass.isFrozen()) {
                targetCtClass.defrost();
            }

            CtMethod[] methods = sourceCtClass.getDeclaredMethods();
            // 获取 SdkContext 的 CtClass
            CtClass sdkContext = EnhanceUtil.pool.getCtClass(EnhanceUtil.SDK_CONTEXT_CLASS);
            // 获取原始类的常量池
            // 知识点：《深入理解Java虚拟机》常量池
            ConstPool constPool = targetCtClass.getClassFile().getConstPool();
            // 遍历原始类的所有方法，逐个拷贝，添加 SdkContext 方法参数
            for (CtMethod sourceCtMethod : methods) {
                Method sourceMethod = openApiInterface.getDeclaredMethod(sourceCtMethod.getName(), getParameterTypes(sourceCtMethod));
                // 拷贝原始方法，注意：拷贝得到的方法不带注解
                CtMethod targetCtMethod = CtNewMethod.copy(sourceCtMethod, targetCtClass, null);

                // 判断是否需要自动注入 SdkContext.class
                boolean needAutoType = needAutoType(sourceMethod);
                if (needAutoType) {
                    targetCtMethod.addParameter(sdkContext);
                }

                // 知识点：《深入理解Java虚拟机》Class文件结构中的 method_info 方法表集合
                MethodInfo methodInfo = targetCtMethod.getMethodInfo();

                // 知识点：《深入理解Java虚拟机》Class文件结构中的 attribute_info 属性表集合
                // Class 文件、字段表、方法表都有自己都属性表集合
                List<AttributeInfo> attributes = sourceCtMethod.getMethodInfo().getAttributes();
                for (AttributeInfo attribute : attributes) {
                    // 运行时可见的注解，实际上就是运行时就是进行反射调用可见的
                    // 知识点：《深入理解Java虚拟机》属性表的 RuntimeVisibleAnnotations
                    if (attribute instanceof AnnotationsAttribute) {
                        // 拷贝方法上的注解
                        AttributeInfo methodAnnotation = attribute.copy(constPool, null);
                        methodInfo.addAttribute(methodAnnotation);
                    }
                    // Java具有范型擦除，范型情况下的方法签名，记录了真实的类型
                    // 知识点：《深入理解Java虚拟机》属性表的  Signature
                    if (attribute instanceof SignatureAttribute) {
                        // 范型处理
                        AttributeInfo methodAnnotation = attribute.copy(constPool, null);
                        methodInfo.addAttribute(methodAnnotation);
                    }

                    if (attribute instanceof ParameterAnnotationsAttribute) {
                        // 参数注解
                        // 注意：因为手动新增了 SdkContext 方法参数，不能像方法注解一样直接拷贝，会抛出方法参数不匹配的异常
                        EnhanceUtil.copyParameterAnnotationsAttribute(constPool, methodInfo, attribute, needAutoType);
                    }
                }

                String newSignature = methodInfo.toString();
                targetKeySourceMethodMap.put(newSignature, sourceMethod);
                targetCtClass.addMethod(targetCtMethod);
            }

            Class<?> newClass = targetCtClass.toClass();
            this.targetCtClass = targetCtClass;
            return newClass;
        } catch (NotFoundException | CannotCompileException e) {
            throw new JavassistException(e);
        } catch (ClassNotFoundException e) {
            throw new JavassistException(e);
        } catch (NoSuchMethodException e) {
            throw new JavassistException(e);
        } catch (Exception e) {
            throw new JavassistException(e);
        }
    }

    private void checkDuplicateMethodName(Class openApiInterface) {

        Set<String> methodNames = new HashSet<>();

        Method[] declaredMethods = openApiInterface.getDeclaredMethods();

        if (declaredMethods == null || declaredMethods.length == 0) {
            throw new NotFoundAnyOpenApiDefinitionException("Not found any open api definition in class " + openApiInterface.getName());
        }

        for (Method declaredMethod : declaredMethods) {
            if (methodNames.contains(declaredMethod.getName())) {
                throw new OverloadMethodsException("can't define overload method in class " + openApiInterface.getName());
            }
            methodNames.add(declaredMethod.getName());
        }
    }

    private boolean needAutoType(Method method) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes != null && parameterTypes.length > 0) {
            return !Arrays.stream(parameterTypes).anyMatch(v -> v.equals(SdkContext.class));
        }
        return true;
    }

    private boolean hasSdkContextParameterType(Method method) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes != null && parameterTypes.length > 0) {
            return Arrays.stream(parameterTypes).anyMatch(v -> v.equals(SdkContext.class));
        }
        return false;
    }

    public Class getOpenApiInterface() {
        return openApiInterface;
    }

    public Class getEnhanceInterface() {
        return enhanceInterface;
    }

    public Method getMethod(Method method) {
        Method targetMethod = methodMapping.get(method);
        return targetMethod;
    }

    public MethodWrapper getMethodWrapper(Method method) {
        return methodMethodWrapperMap.get(method);
    }

    public static boolean isCollection(CtClass ctClass) {
        return EnhanceUtil.isAssignable(ctClass, Collection.class.getName());
    }

    public static boolean isParameterizedType(Type type) {
        return type instanceof ParameterizedType;
    }
}
